package org.xx.armory.spring5.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.xx.armory.commons.ForLogging;

import java.util.Collection;
import java.util.HashSet;

import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.xx.armory.commons.Validators.rejectIfNotInstance;
import static org.xx.armory.spring5.security.WebSecurityConfig.ROLE_ANONYMOUS;

/**
 * 使用用户名和口令进行身份认证的提供程序的抽象实现。
 * <p>仅支持 {@link UsernamePasswordAuthenticationToken} 类型的凭据。</p>
 */
public abstract class AbstractArmoryUserPasswordAuthenticationProvider
        implements AuthenticationProvider {
    public static final String CAPTCHA_CODE_KEY = "__captcha_code__";

    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(AbstractArmoryUserPasswordAuthenticationProvider.class);

    @Override
    public Authentication authenticate(
            Authentication authentication
    )
            throws AuthenticationException {
        final var token = rejectIfNotInstance(authentication, UsernamePasswordAuthenticationToken.class, "authentication");

        final var credentials = token.getCredentials();

        if (!(credentials instanceof CharSequence)) {
            return null;
        }

        final var username = trimToEmpty(token.getName());
        final var password = (CharSequence) credentials;
        final var details = authentication.getDetails();
        final var aDetails = details instanceof ArmoryWebAuthenticationDetails ?
                (ArmoryWebAuthenticationDetails) details : null;

        if (username.isEmpty() || password.length() == 0) {
            throw new InsufficientAuthenticationException("Username or password required");
        } else if (aDetails != null && !aDetails.isCaptchaCodeMatched()) {
            throw new BadCredentialsException("Captcha code mismatched");
        } else {
            final var authorities_ = this.authenticate(username, password, aDetails);
            if (authorities_ == null) {
                return null;
            }

            final var authorities = new HashSet<GrantedAuthority>(authorities_);

            // 如果没有任何授权，那么授予访客权限。
            if (authorities.isEmpty()) {
                authorities.add(new SimpleGrantedAuthority(ROLE_ANONYMOUS));
            }

            return new UsernamePasswordAuthenticationToken(
                    authentication.getPrincipal(),
                    authentication.getCredentials(),
                    authorities
            );
        }
    }

    @Override
    public boolean supports(
            Class<?> aClass
    ) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
    }

    /**
     * 通过用户名和口令进行身份认证。
     * <p>如果希望将此凭据留给下一个验证器则返回{@code null}。</p>
     *
     * @param username
     *         用户名，保证不是 {@code null}。
     * @param password
     *         口令，保证不是 {@code null}。
     * @param details
     *         认证明细，如果认证明细无法被转化为 {@link ArmoryWebAuthenticationDetails} 类型，则此参数是 {@code null}。
     * @return 通过认证的权限集合，如果授予任何权限则返回空集合。
     * @throws AuthenticationException
     *         如果指定的用户名不存在，或者用户名和口令不匹配，或者出现其它错误。
     */
    protected abstract Collection<? extends GrantedAuthority> authenticate(
            String username,
            CharSequence password,
            ArmoryWebAuthenticationDetails details
    )
            throws AuthenticationException;
}
